package com.example.demo.test;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.extra.spring.SpringUtil;
import org.apache.commons.lang3.StringUtils;
import org.apache.ibatis.session.SqlSessionFactory;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Controller;
import org.springframework.stereotype.Repository;
import org.springframework.stereotype.Service;
import org.springframework.util.MultiValueMap;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.builders.ResponseMessageBuilder;
import springfox.documentation.schema.ModelRef;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.service.Contact;
import springfox.documentation.service.ResponseMessage;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spi.service.DocumentationPlugin;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.spring.web.plugins.DocumentationPluginsBootstrapper;

import java.lang.reflect.Modifier;
import java.net.URL;
import java.util.*;

/**
 * 基于spring的应用上下文提供一些工具方法
 *
 * @author 张述江
 * @date 2023-08-25
 */
public class ModuleApplication {
    private final static String SINGLETON = "singleton";
    private final static String DYNAMIC_DOC_PACKAGE = "dynamic.swagger.doc.package";
    private final Set<RequestMappingInfo> extMappingInfos = new HashSet<>();

    /**
     * 加载一个外部扩展jar，包括springmvc接口资源，mybatis的@mapper和mapper.xml和spring bean等资源
     *
     * @param url               jar url
     */
    public void reloadJar(URL url) {
        URL[] urls = new URL[]{url};
        /*
         * 这里实际上是将spring的ApplicationContext的类加载器当成parent传给了自定义类加载器，很明自定义的子类加载器自己加载
         * 的类，parent类加载器直接是获取不到的，所以在自定义类加载器做了特殊的骚操作
         */
        ModuleClassLoader moduleClassLoader = new ModuleClassLoader(SpringUtil.getApplicationContext().getClassLoader(), urls);
        /*
         * 使用模块类加载器加载url资源的jar包，直接返回类的全限定名和Class对象的映射，这些Class对象是
         * jar包里所有.class结尾的文件加载后的结果,同时mybatis的xml加载后，无奈的放入了
         * moduleClassLoader.getXmlBytesMap()，不是很优雅
         */
        Map<String, Class<?>> classMap = moduleClassLoader.load();
        MapperLoader mapperLoader = new MapperLoader();
        /*
         * 刷新mybatis的xml和Mapper接口资源，Mapper接口其实就是xml的namespace
         */
        Map<String, Object> extObjMap = mapperLoader.refresh(SpringUtil.getBean(SqlSessionFactory.class), moduleClassLoader.getXmlBytesMap());
        /*
         * 将各种资源放入spring容器
         */
        registerBeans(classMap, extObjMap);
    }

    /**
     * 装载bean到spring中
     *
     * @param cacheClassMap
     */
    public void registerBeans(Map<String, Class<?>> cacheClassMap, Map<String, Object> extObjMap) {
        /*
         * 获取bean工厂并转换为DefaultListableBeanFactory
         */
        DefaultListableBeanFactory defaultListableBeanFactory = (DefaultListableBeanFactory) SpringUtil.getBeanFactory();
        /*
         * 有一些对象想给spring管理，则放入spring中，如mybatis的@Mapper修饰的接口的代理类
         */
        if (extObjMap != null && !extObjMap.isEmpty()) {
            extObjMap.forEach((beanName, obj) -> {
                /*
                 * 如果已经存在，则销毁之后再注册
                 */
                if (defaultListableBeanFactory.containsSingleton(beanName)) {
                    defaultListableBeanFactory.destroySingleton(beanName);
                }
                defaultListableBeanFactory.registerSingleton(beanName, obj);
            });
        }
        for (Map.Entry<String, Class<?>> entry : cacheClassMap.entrySet()) {
            String className = entry.getKey();
            Class<?> clazz = entry.getValue();
            if (isSpringBeanClass(clazz)) {
                // 将变量首字母置小写
                String beanName = StringUtils.uncapitalize(className);
                beanName = beanName.substring(beanName.lastIndexOf(".") + 1);
                beanName = StringUtils.uncapitalize(beanName);
                /*
                 * 已经在spring容器就删了
                 */
                if (defaultListableBeanFactory.containsBeanDefinition(beanName)) {
                    defaultListableBeanFactory.removeBeanDefinition(beanName);
                }
                /*
                 * 使用spring的BeanDefinitionBuilder将Class对象转成BeanDefinition
                 */
                BeanDefinitionBuilder beanDefinitionBuilder = BeanDefinitionBuilder.genericBeanDefinition(clazz);
                BeanDefinition beanDefinition = beanDefinitionBuilder.getRawBeanDefinition();
                //设置当前bean定义对象是单利的
                beanDefinition.setScope(SINGLETON);
                /*
                 *  以指定beanName注册上面生成的BeanDefinition
                 */
                defaultListableBeanFactory.registerBeanDefinition(beanName, beanDefinition);
            }
        }
        /*
         * 刷新springmvc，让新增的接口生效
         */
        refreshMVC();
    }

    /**
     * 刷新springMVC,这里花了大量时间调试，找不到开放的方法，只能取个巧，在更新RequestMappingHandlerMapping前先记录之前
     * 所有RequestMappingInfo，记得这里一定要copy一下，然后刷新后再记录一次，计算出差量存放在成员变量Set中，然后每次开头判断
     * 差量那里是否有内容，有就先unregiester掉
     */
    private void refreshMVC() {
        /*
         * 先拿到RequestMappingHandlerMapping对象
         */
        RequestMappingHandlerMapping mappingHandlerMapping = SpringUtil.getBean("requestMappingHandlerMapping");
        /*
         * 重新注册mapping前先判断是否存在了，存在了就先unregister掉
         */
        if (!extMappingInfos.isEmpty()) {
            for (RequestMappingInfo requestMappingInfo : extMappingInfos) {
                mappingHandlerMapping.unregisterMapping(requestMappingInfo);
            }
        }
        /*
         * 获取刷新前的RequestMappingInfo
         */
        Map<RequestMappingInfo, HandlerMethod> preMappingInfoHandlerMethodMap = mappingHandlerMapping.getHandlerMethods();
        /*
         * 这里注意一定要拿到拷贝，不然刷新后内容就一致了，就没有差量了
         */
        Set<RequestMappingInfo> preRequestMappingInfoSet = new HashSet<>(preMappingInfoHandlerMethodMap.keySet());
        /*
         * 这里是刷新springmvc上下文
         */
        SpringUtil.getBeanFactory().getBeansOfType(RequestMappingHandlerMapping.class)
                .forEach((key, value) -> {
                    value.afterPropertiesSet();
                });
        /*
         * 获取刷新后的RequestMappingInfo
         */
        Map<RequestMappingInfo, HandlerMethod> afterMappingInfoHandlerMethodMap = mappingHandlerMapping.getHandlerMethods();
        Set<RequestMappingInfo> afterRequestMappingInfoSet = afterMappingInfoHandlerMethodMap.keySet();
        /*
         * 填充差量部分RequestMappingInfo
         */
        fillSurplusRequestMappingInfos(preRequestMappingInfoSet, afterRequestMappingInfoSet);
        /*
         * 这里真的是不讲武德了，每次调用value.afterPropertiesSet();如下urlLookup都会产生重复，暂时没找到开放方法去掉重复，这里重复会导致
         * 访问的时候报错Ambiguous handler methods mapped for
         * 目标是去掉RequestMappingHandlerMapping -> RequestMappingInfoHandlerMapping -> AbstractHandlerMethodMapping
         * -> mappingRegistry -> urlLookup重复的RequestMappingInfo,这里的.getClass().getSuperclass().getSuperclass()相信会
         * 很懵逼，如果单独通过getClass().getDeclaredMethod("getMappingRegistry",new Class[]{})是无论如何都拿不到父类的非public非
         * protected方法的，因为这个方法不属于子类，只有父类才可以访问到，只有你拿得到你才有资格不讲武德的使用method.setAccessible(true)强行
         * 访问
         */
        MultiValueMap<String, RequestMappingInfo> multiValueMap = BeanUtil.getProperty(mappingHandlerMapping, "mappingRegistry.pathLookup");
        multiValueMap.forEach((key, list) -> {
            clearMultyMapping(list);
        });

        /*
         * 刷新swagger文档
         */
        refreshSwagger();
    }

    /**
     * 填充差量的RequestMappingInfo，因为已经重写过hashCode和equals方法所以可以直接用对象判断是否存在
     *
     * @param preRequestMappingInfoSet
     * @param afterRequestMappingInfoSet
     */
    private void fillSurplusRequestMappingInfos(Set<RequestMappingInfo> preRequestMappingInfoSet, Set<RequestMappingInfo> afterRequestMappingInfoSet) {
        for (RequestMappingInfo requestMappingInfo : afterRequestMappingInfoSet) {
            if (!preRequestMappingInfoSet.contains(requestMappingInfo)) {
                extMappingInfos.add(requestMappingInfo);
            }
        }
    }

    /**
     * 简单的逻辑，删除List里重复的RequestMappingInfo，已经写了toString，直接使用mappingInfo.toString()就可以区分重复了
     *
     * @param mappingInfos
     */
    private void clearMultyMapping(List<RequestMappingInfo> mappingInfos) {
        Set<String> containsList = new HashSet<>();
        for (Iterator<RequestMappingInfo> iter = mappingInfos.iterator(); iter.hasNext(); ) {
            RequestMappingInfo mappingInfo = iter.next();
            String flag = mappingInfo.toString();
            if (containsList.contains(flag)) {
                iter.remove();
            } else {
                containsList.add(flag);
            }
        }
    }


    /**
     * 刷新swagger文档
     */
    private void refreshSwagger() {
        /*
         * 获取扩展包swagger的地址接口扫描包，如果有配置则执行文档刷新操作
         */
        String extSwaggerDocPackage = SpringUtil.getProperty(DYNAMIC_DOC_PACKAGE);
        if (StringUtils.isBlank(extSwaggerDocPackage)) {
            return;
        }
        /*
         * 拿到swagger解析文档的入口类，真的不想这样，主要是根本不提供刷新和重新加载文档的方法，只能不讲武德了
         */
        DocumentationPluginsBootstrapper bootstrapper = SpringUtil.getBean(DocumentationPluginsBootstrapper.class);
        /*
         * 拿到最关键的文档插件集合，所有逻辑文档解析逻辑都在插件中
         */
        List<DocumentationPlugin> modifyerList = BeanUtil.getProperty(bootstrapper, "documentationPluginsManager.documentationPlugins.plugins");
        /*
         * 这下老实了吧，把自己的Docket加入进去，这里的groupName为dynamic
         */
        Docket restApi = createRestApi(extSwaggerDocPackage);
        modifyerList.removeIf(x -> x.getGroupName().equals(restApi.getGroupName()));
        modifyerList.add(restApi);
        /*
         * 清空罪魁祸首DocumentationCache缓存，不然就算再加载一次，获取文档还是从这个缓存中拿，不会完成更新
         */
        bootstrapper.stop();
        /*
         * 手动执行重新解析swagger文档
         */
        bootstrapper.start();
    }

    public Docket createRestApi(String basePackage) {
        List<ResponseMessage> responseMessageList = new ArrayList<>();
        responseMessageList.add(new ResponseMessageBuilder().code(200).message("成功").responseModel(new ModelRef("Payload")).build());
        return new Docket(DocumentationType.SWAGGER_2)
                .groupName("dynamic")
                .globalResponseMessage(RequestMethod.GET, responseMessageList)
                .globalResponseMessage(RequestMethod.DELETE, responseMessageList)
                .globalResponseMessage(RequestMethod.POST, responseMessageList)
                .apiInfo(apiInfo()).select()
                //为当前包路径
                .apis(RequestHandlerSelectors.basePackage(basePackage)).paths(PathSelectors.any()).build();
    }

    /**
     * 构建api文档的详细信息函数
     */
    private ApiInfo apiInfo() {
        return new ApiInfoBuilder()
                // 页面标题
                .title("SpringBoot动态扩展")
                // 创建人
                .contact(new Contact("rongdi", "https://www.cnblogs.com/rongdi", "495194630@qq.com"))
                // 版本号
                .version("1.0")
                // 描述
                .description("api管理").build();
    }


    /**
     * 判断class对象是否带有spring的注解
     */
    public static boolean isSpringBeanClass(Class<?> cla) {
        /*
         * 如果为空或是接口或是抽象类直接返回false
         */
        if (cla == null || cla.isInterface() || Modifier.isAbstract(cla.getModifiers())) {
            return false;
        }

        Class targetClass = cla;
        while (targetClass != null) {
            /*
             * 如果包含spring注解则返回true
             */
            if (targetClass.isAnnotationPresent(Component.class) ||
                    targetClass.isAnnotationPresent(Repository.class) ||
                    targetClass.isAnnotationPresent(Service.class) ||
                    targetClass.isAnnotationPresent(Configuration.class) ||
                    targetClass.isAnnotationPresent(Controller.class) ||
                    targetClass.isAnnotationPresent(RestController.class)) {
                return true;
            }
            targetClass = targetClass.getSuperclass();
        }

        return false;
    }
}
